上一篇文章中,我們探討了 Django Ninja 影響 API 文件呈現的一些重要設定。它們是自動化 API 文件的基本功,不容忽視。
但這樣還不夠!我們想要讓這份文件更加生動,讀起來清晰易懂。
其中的關鍵在於 API 文件上的資料範例。好的範例讓人一讀就懂,能有效縮短理解和思考的時間。
本文將介紹如何運用 Pydantic 的Field
設定,全方位提升 API 文件的清晰與可讀性。我們會探討如何為自動生成的文件加上栩栩如生的範例,讓文件更貼近真實。
本文所有的程式碼變動,可參考這個 PR。
Pydantic 是一個實現資料驗證、序列化的套件,廣泛應用於 FastAPI 和 Django Ninja 等框架。
在 Django Ninja 中,Pydantic 被用來定義 Schema,這些 Schema 決定了 API 如何處理 HTTP 請求和回應中的資料,並自動轉換成符合 OpenAPI 標準的文件。
Pydantic 的強大之處在於,它不僅能驗證資料,還可以通過Field
設定,為文件欄位提供額外的說明、範例和預設值。
這些細節設定會自動反映在轉換後的 API 文件中,幫助開發者更好地理解 API 的行為與內涵。
Pydantic 的Field
是一個強大的工具,可以用來為每個資料欄位提供更多細節資訊,如標題、描述、範例和預設值等。
這些設定不僅有助於數據驗證,也大大提升了 API 文件的可讀性。以下是一些常見的Field
參數:
title
:為欄位設定標題,幫助開發者快速理解該欄位的作用。description
:提供欄位描述,讓人更清楚地了解這個欄位的用途與限制。examples
:設定範例值,幫助開發者直觀理解 API 的輸入、輸出格式。default
:第一位置參數,提供欄位的預設值。Input 未提供該欄位值時,將自動使用預設值。善用這些參數,可以產生高品質的 API 文件。
不過!我們還是要稍稍向「現實」靠攏,如果每一個 API 都要你寫這麼多內容,可能會讓開發者感到負擔過重。
而且,使用大量參數,文件確實變好看了,但產生文件的程式碼不免會落落長!
我們要找到一個平衡點,既能提供足夠資訊,又不會讓程式變得過於冗長。
從這個角度考慮,我覺得其中最重要的兩個參數,是default
和examples
——尤其是後者!
所以本文會專注介紹這兩者,這樣不僅學習上更聚焦,也符合我的開發日常。
如果你想多了解 Pydantic Field 的參數與用法,那就要看 Pydantic 的官方文件——而不是 Django Ninja。
Django Ninja 的文件中,並沒有專門的章節介紹Field
的使用。這是因為Field
實際上是 Pydantic 的功能,而不是 Django Ninja 特有的。
不過如果你真的去看這份文件,可能會發現,它對Field
全部參數的解說,也不算是非常詳盡。
想要知道所有可用的參數,我覺得看原始碼是最快的。然後從函式簽名(對,Field
是一個函式)的 type hints 去揣摩它的用法,也不失為一個好的方式。
以下,我們開始講述如何使用 Pydantic Field 的examples
和default
參數,讓 API 文件更加生動且嚴謹。
上一篇我們提到目前 API 文件的不足,其中「缺乏真實範例」這個問題還未解決。
以下是「取得單一文章資訊」在文件中的回應範例:
{
"id": 0,
"title": "string",
"content": "string",
"author": {
"id": 0,
"username": "string",
"email": "string"
},
"created_at": "2024-09-22T08:58:55.960Z",
"updated_at": "2024-09-22T08:58:55.960Z"
}
無論0
或"string"
,都稱不上是好的文件範例——它們都不夠真實。
現在,我們要為回應的 Schema 加上範例,程式碼如下:
class _AuthorInfo(Schema):
id: int = Field(examples=[1])
username: str = Field(examples=['Alice'])
email: str = Field(examples=['alice@exapmple.com'])
class PostResponse(Schema):
id: int = Field(examples=[1])
title: str = Field(examples=['Ninja is awesome!'])
content: str = Field(examples=['This is my first post.'])
author: _AuthorInfo
created_at: datetime = Field(examples=['2021-01-01T00:00:00Z'])
updated_at: datetime = Field(examples=['2021-01-01T00:00:00Z'])
...
這裡有兩個重點。
我只有使用examples
參數,這樣最簡單,而且範例確實是文件中相當重要的一環。
此外,這個examples
其實大有文章,如果你寫成example
,比如:
class PostResponse(Schema):
id: int = Field(example=1)
實際上也能正常運作,但 Mypy 卻會提醒你:
Unexpected keyword argument "example" for "Field"; did you mean "examples"?
沒錯,因為現在的 Pydantic v2,Field
只有examples
這個參數。example
應該是 Pydantic v1 的做法,而 Django Ninja 還保持了對二者的相容。
考慮到未來,建議還是使用examples
,不僅可以避免 Mypy 的警告,而且與最新版本的 Pydantic 保持一致。
巢狀 Schema 的範例,只要在底層 Schema 加上Field
即可。引用層不必聲明:
class _AuthorInfo(Schema): # 這是巢狀底層,要寫 Field
id: int = Field(examples=[1])
username: str = Field(examples=['Alice'])
email: str = Field(examples=['alice@exapmple.com'])
class PostResponse(Schema):
...
author: _AuthorInfo # 無須再寫 Field
...
看一下實際的 API 文件回應,我直接截取頁面中的 JSON 值:
{
"id": 1,
"title": "Ninja is awesome!",
"content": "This is my first post.",
"author": {
"id": 1,
"username": "Alice",
"email": "alice@exapmple.com"
},
"created_at": "2021-01-01T00:00:00Z",
"updated_at": "2021-01-01T00:00:00Z"
}
相比於之前的0
、"string"
,是不是更生動、好讀了呢?
在我看來,大部分的時候,我們並不需要定義預設值。
我建議你也不要這樣寫:
class PostResponse(Schema):
id: int = 1
...
雖然文件上一樣也會顯示範例值為 1,但其實這個寫法與下面這個寫法等價:
class PostResponse(Schema):
id: int = Field(default=1)
...
這實際上是在定義預設值。如前所述,如果 Schema 用在 HTTP 請求,且客戶端未提供該欄位的值時,Django Ninja 將自動使用預設值。
這很可能會造成出乎意料的結果。
正確的流程是:前端沒有提供值的時候,Django Ninja 應該要給出 422 回應。
所以你根本不需要(也不應該)定義預設值——除了下列情況。
我個人推薦,只在請求欄位為「可選(optional)」時,使用default
參數。
而且此時的預設值應為None
。
為了示範,我們建立一個新 API——「新增使用者」(也就是用戶註冊)。這個 API 在後續教學中,還會被反覆提及與改進。
還記得我們的User
模型中,bio
欄位是可選的嗎?
class User(AbstractUser):
email = models.EmailField(unique=True) # 強制唯一的 email
bio = models.TextField(null=True) # 個人簡介欄位(可選)
...
所以,我們的 API 請求 Schema 如下——直接看bio
欄位設定:
class CreateUserRequest(Schema):
...
bio: str | None = Field(
default=None,
examples=['Hello, I am Alice.']
)
Field 中的default=None
設定讓你在客戶端沒有填入值時,API 也不會出錯。
另外留意bio: str | None
這個 type hint,千萬不要少了None
,會影響文件的渲染結果:(這是有None
的結果)
有None
,API 文件才會顯示欄位值為可選(string | null)。
經過本章的學習與改進,我們的 API 文件已經達到 80 分水準!在大多數開發專案中,這樣的文件品質可以說相當出色了。
接下來我們要進入第五章——資料驗證與錯誤處理。
這個章節將涵蓋如何在 Django Ninja 中實現有效的資料驗證,以及如何優雅地處理和回應各種可能的錯誤情況。
通過這些技巧,我們將能夠建立更穩健、可靠的 API。
本文同步發表於我的部落格——Code and Me
假設我每個 examples 都給兩個範例,目前 Ninja 在沒有額外設定的情況下似乎永遠只使用第一個,沒有下拉式選單可以選擇多個 example
class CreateUserRequest(Schema):
username: str = Field(examples=['Alice', 'Bob'])
email: str = Field(examples=['alice@example.com', 'bob@example.com'])
password: str = Field(min_length=8, examples=['password123', 'password456'])
如何讓 Ninja 在自生成的 API 文件在套用到 swagger 上時候能產生多個範例選擇呢? 類似這個 stackoverflow 中 討論 FastAPI 如何實現同樣問題的情況
是的,就是會這樣,你也太認真!哈哈哈
有兩個解法:
Field(example=['alice@example.com', 'bob@example.com'])
# 或
Field(examples=[['alice@example.com', 'bob@example.com']])
效果:
{
"username": "Alice",
"email": [
"alice@example.com",
"bob@example.com"
],
"password": "password123",
"confirm_password": "password123",
"bio": "Hello, I am Alice."
}
這個例子感覺怪怪der,因為 email 欄位本身不是 list,這個範例可能會讓使用者誤以為這個欄位要輸入一個陣列,用這個 example 去打也會得到 422
看了一下感覺跟 OpenAPI 3.1.0 有關,也許不是那麼好做到
懂你的意思了,我之前沒有看到連結的下面出現的「選單」XD
那我確實沒做過,等待你的研究!
在 fastapi 大概知道怎做,但也是挺麻煩的
Ninja 就不清楚了=口=